﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;

namespace InfiniTec.Threading
{
	[Localizable(false)]
	public static class Dispatcher
	{
		private static Thread _DispatcherThread;
		private static readonly List<DispatcherCookie> _PendingCallbacks = new List<DispatcherCookie>();
		private static readonly object _PendingCallbacksLock = new object();
		private static readonly AutoResetEvent _DispatcherSignal = new AutoResetEvent(false);

		public static DispatcherCookie RegisterCallback(TimeSpan delay, WaitCallback callback)
		{
			return RegisterCallback(delay, callback, null);
		}

		public static DispatcherCookie RegisterCallback(TimeSpan delay, WaitCallback callback, object state)
		{
			if (delay <= TimeSpan.Zero) throw new ArgumentOutOfRangeException("delay", delay, "The delay must be positive");
			return RegisterCallback(DateTime.Now + delay, callback, state);
		}
		public static DispatcherCookie RegisterCallback(DateTime callbackTime, WaitCallback callback)
		{
			return RegisterCallback(callbackTime, callback, null);
		}

		public static DispatcherCookie RegisterCallback(DateTime callbackTime, WaitCallback callback, object state)
		{
			if (callback == null) throw new ArgumentNullException("callback");
			if (callbackTime < DateTime.Now) throw new ArgumentOutOfRangeException("callbackTime", callbackTime, "The callback time must be in the future.");

			var cookie = new DispatcherCookie(callbackTime, callback, state);

			int index;
			lock (_PendingCallbacksLock)
			{
				if (_DispatcherThread == null)
				{
					InitializeDispatcherThread();
				}
				index = _PendingCallbacks.Count - 1;
				while (index >= 0 && _PendingCallbacks[index].CallbackTime > callbackTime) index--;

				_PendingCallbacks.Insert(index + 1, cookie);
			}

			if (index == -1)
			{
				_DispatcherSignal.Set();
			}

			return cookie;
		}

		public static bool TryCancelCallback(DispatcherCookie cookie)
		{
			if (cookie == null) throw new ArgumentNullException("cookie");

			lock (_PendingCallbacksLock)
			{
				return _PendingCallbacks.Remove(cookie);
			}
		}
		public static void CancelCallback(DispatcherCookie cookie)
		{
			if (!TryCancelCallback(cookie))
			{
				throw new ArgumentException("The cookie is not registered on the dispatcher.");
			}
		}

		private static void InitializeDispatcherThread()
		{
			_DispatcherThread = new Thread(DispatcherThread) { Name = "infinitec.threading Dispatcher thread", IsBackground = true };

			_DispatcherThread.Start();
		}

		private static void DispatcherThread()
		{
			while (true)
			{
				DispatcherCookie cookie = null;
				TimeSpan? delay;

				lock (_PendingCallbacksLock)
				{
					var now = DateTime.Now;
					delay = null;

					if (_PendingCallbacks.Count > 0)
					{
						cookie = _PendingCallbacks[0];
						if (cookie.CallbackTime <= now)
						{
							_PendingCallbacks.RemoveAt(0);
						}
						else
						{
							delay = cookie.CallbackTime - now;
							cookie = null;
						}
					}
				}

				if (delay != null)
				{
					_DispatcherSignal.WaitOne(delay.Value, true);
				}
				else if (cookie == null)
				{
					_DispatcherSignal.WaitOne();
				}
				else
				{
					ThreadPool.QueueUserWorkItem(ExecuteCallback, cookie);
				}
			}
		}

		private static void ExecuteCallback(object state)
		{
			var cookie = (DispatcherCookie)state;

			try
			{
				cookie.Callback(cookie.State);
			}
			catch (Exception ex)
			{
				AsyncOperation.Trace.Error("Failed to execute callback. Error: {0}", ex);
			}
		}
	}
}
